iT邦幫忙

2023 iThome 鐵人賽

DAY 8
1

該怎麼讓舊的 function 兼容 Option (Either 也適用)

也許你用的 function 在很多地方都用到,改變原 function 的定義影響甚大,也或者該 function 是寫在別的 module 裡,這時候你想用 Option 的話怎麼辦呢?

這時候我們可以 Lift (抬起來) 這些原始 function,並讓它們支持 Option 操作,請看以下程式,

def lift[A, B](f: A => B): Option[A] => Option[B] = _ map f

Option 的 map 就是用 function f 改變其值,然後在回傳 Option,而 lift 就是在多一層加工,把 f 包裝成 partial function 後回傳,

在 Scala 中,_ map f 等於 _.map(f)it's syntactic sugar 😄

以下程式我們把原本不是回傳 Option 的開根號 function,搖身一變成回傳 Option 的開根號 function 了。

val sqrtOpt: Option[Double] => Option[Double] = lift(math.sqrt)
sqrtOpt(Some(4))

Lift 有 1 就有 2,map2 function

如果原生 function 是需要傳入 2 個參數的呢?例如 max function,

 def max(x: Int, y: Int): Int

然後我們想要這樣使用,找到比較大的年紀,

def maxAge(age1: String, age2: String): Option[Int] =
  val age1Opt = Try(age1.toInt)
  val age2Opt = Try(age2.toInt)
  
	math.max(age1Opt, age2Opt) // 會發生編譯錯誤


def Try[A](a: => A): Option[A] =
  try
    Some(a)
  catch
    case _: Exception => None

這裡的 Try 可視為一個萬用 function,幫助我們在有錯誤時回傳 None,實際上在 Scala 原生庫中已經有類似的 Try 資料型別來把 try-catch 包裹成 Success 和 Failure。

此時我們可以使用 lift 方式,設計一個 function map2 來讓 max 支援 Option,

def map2[A, B, C](a: Option[A], b: Option[B])(f: (A, B) => C): Option[C] =
  a.flatMap(aa => b.map(bb => f(aa, bb)))

flatMap 的 high-order function 要回傳的東西是 Option,所以我們就把 b 當做這個 Option 回傳,但我們的目的是調用 f,所以我們調用 b Option 的 map,在把裡頭的值傳入 f 取得結果,

如此我們就可以讓 math.max 支持 Option 型別了。

def maxAge(age1: String, age2: String): Option[Int] =
  val age1Opt = Try(age1.toInt)
  val age2Opt = Try(age2.toInt)
  
	map2(age1Opt, age2Opt)(math.max)

for-comprehensions

因為 lifting 這種功能的 function 在 Scala 很常見,所以 Scala 有提供一種叫 for-comprehensions 的語法,使其能自動擴展成 flatMap 和 map 呼叫,所以我們就能把 map2 改成下面這個樣子,

  def map2_1[A, B, C](a: Option[A], b: Option[B])(f: (A, B) => C): Option[C] =
    for {
      aa <- a
      bb <- b
    } yield f(aa, bb)

看起來比較直觀了,Scala 會把 yeild 前的綁定 bb <- b 當作 map 呼叫,而其它都是使用 flatMap 呼叫。

總結

Exception 的拋出會違反 Referential Transparency,所以 functional programming 處理錯誤的方式就是將錯誤視為某一種應該要返回的回傳值,其概念有點像 結構化程式語言 回傳錯誤碼那樣,但 FP 多包裹了一層,使其我們可以從這之取得正常值和錯誤值,所以我們設計了 OptionEither 等型別來使其符合 RT;

在來就是介紹了如何在不改寫既有 function 定義的情況下,使其支援 OptionEither 型別,最後就是如何使用 Scala 的 for-comprehensions 改寫 flatMap 和 map 的呼叫,讓我們看的順眼一些。


上一篇
如何不拋出例外的處理錯誤 (2)
下一篇
Strictness 和 Laziness (1)
系列文
用 Scala 3 寫的 Functional Programming 會長什麼樣子?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言